package de.jmda.core.mproc;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ElementVisitor;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVisitor;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleElementVisitor7;
import javax.lang.model.util.SimpleTypeVisitor7;
import javax.lang.model.util.Types;

import org.apache.commons.lang3.StringUtils;

/**
 * One stop access point for all annotation processing utilities provided by
 * jMDA.
 *
 * @author roger@jmda.de
 */
public class ProcessingUtilities
{
	/**
	 * singleton instance, initialised lazily by {@link
	 * #instance(ProcessingEnvironment, Set, RoundEnvironment)}
	 */
	private static ProcessingUtilities processingUtilities;

	// fields that will be populated by constructor
	private ProcessingEnvironment processingEnvironment;
	private Set<? extends TypeElement> supportedAnnotations;
	private RoundEnvironment roundEnvironment;

	/**
	 * Internal shortcut to {@link ProcessingEnvironment#getElementUtils()}.
	 */
	private Elements elementUtilities;

	/**
	 * Internal shortcut to {@link ProcessingEnvironment#getTypeUtils()}.
	 */
	private Types typeUtilities;

	private WildcardType wildcardTypeNull;

// TODO experimental
//
//	private DeclaredType declaredTypeCollection;
//	private DeclaredType declaredTypeMap;
//	private DeclaredType declaredTypeVoid;


	static synchronized ProcessingUtilities instance(
			ProcessingEnvironment processingEnvironment,
      Set<? extends TypeElement> supportedAnnotations,
      RoundEnvironment roundEnvironment)
  {
		processingUtilities =
				new ProcessingUtilities(
						processingEnvironment, supportedAnnotations, roundEnvironment);

		return processingUtilities;
  }

	/**
	 * @param processingEnvironment must not be <code>null</code>, otherwise
	 *                              {@link ExceptionInInitializerError} will be
	 *                              thrown
	 * @throws ExceptionInInitializerError see above
	 */
	private ProcessingUtilities(ProcessingEnvironment processingEnvironment)
			throws ExceptionInInitializerError
	{
		super();

		// parameter check
		if (processingEnvironment == null)
		{
			throw new ExceptionInInitializerError("processingEnvironment must not be null");
		}
		// parameter check end

		this.processingEnvironment = processingEnvironment;
		elementUtilities = processingEnvironment.getElementUtils();
		typeUtilities = processingEnvironment.getTypeUtils();

		wildcardTypeNull = typeUtilities.getWildcardType(null, null);

//		// get type elements by names
//		TypeElement typeElementCollection =
//				elementUtilities.getTypeElement(Collection.class.getCanonicalName());
//		TypeElement typeElementMap =
//				elementUtilities.getTypeElement(Map.class.getCanonicalName());
//		TypeElement typeElementVoid =
//				elementUtilities.getTypeElement(Void.class.getCanonicalName());
//		
//		// initialise declared types with correct number of wildcard type parameters
//		declaredTypeCollection =
//				typeUtilities.getDeclaredType(typeElementCollection, wildcardTypeNull);
//		declaredTypeMap =
//				typeUtilities.getDeclaredType(
//						typeElementMap, wildcardTypeNull, wildcardTypeNull);
//		declaredTypeVoid =
//				typeUtilities.getDeclaredType(typeElementVoid);
	}

	private ProcessingUtilities(
			ProcessingEnvironment processingEnvironment,
      Set<? extends TypeElement> supportedAnnotations,
      RoundEnvironment roundEnvironment)
  {
		this(processingEnvironment);
		
		this.supportedAnnotations = supportedAnnotations;
		this.roundEnvironment = roundEnvironment;
  }

	/**
	 * Class for {@link TypeVisitor} objects that provide a {@link DeclaredType}
	 * instance if the objects are accepted by {@link
	 * TypeMirror#accept(TypeVisitor, Object)}. Provided instances are <code>null
	 * </code> if accepting <code>TypeMirror</code> objects do not have {@link
	 * TypeKind#DECLARED}.
	 *
	 * @see ProcessingUtilities#DECLARED_TYPE_PROVIDER_VISITOR
	 * @see ProcessingUtilities#asDeclaredType(TypeMirror)
	 */
	private static class DeclaredTypeProviderVisitor
			extends SimpleTypeVisitor7<DeclaredType, Void>
	{
		@Override
		public DeclaredType visitDeclared(DeclaredType declaredType, Void parameter)
		{
			return declaredType;
		}
	}

	private static class ArrayTypeProviderVisitor
			extends SimpleTypeVisitor7<ArrayType, Void>
	{
		/**
		 * @see javax.lang.model.util.SimpleTypeVisitor6#visitArray(javax.lang.model.type.ArrayType, java.lang.Object)
		 */
		@Override
		public ArrayType visitArray(ArrayType arrayType, Void parameter)
		{
			return arrayType;
		}
	}

	/**
	 * Class for {@link ElementVisitor} objects that provide a {@link TypeElement}
	 * instance if the objects are accepted by {@link
	 * Element#accept(ElementVisitor, Object)}. Provided instances are <code>null
	 * </code> if accepting <code>Element</code> objects do not have {@link
	 * ElementKind#CLASS}.
	 *
	 * @see ProcessingUtilities#TYPE_ELEMENT_PROVIDER_VISITOR
	 * @see ProcessingUtilities#asTypeElement(Element)
	 *
	 * TODO find out why string list and integer objects are not visited as types
	 *      in SimpleElementVisitor6
	 */
	private static class TypeElementProviderVisitor
			extends SimpleElementVisitor7<TypeElement, Void>
	{
		@Override
		public TypeElement visitType(TypeElement typeElement, Void parameter)
		{
			return typeElement;
		}
	}

	private static class ExecutableElementProviderVisitor
			extends SimpleElementVisitor7<ExecutableElement, Void>
	{
		@Override
		public ExecutableElement visitExecutable(ExecutableElement executableElement, Void parameter)
		{
			return executableElement;
		}
	}

	private static class PackageElementProviderVisitor
			extends SimpleElementVisitor7<PackageElement, Void>
	{
		@Override
		public PackageElement visitPackage(PackageElement packageElement, Void parameter)
		{
			return packageElement;
		}
	}

	private static class VariableElementProviderVisitor
			extends SimpleElementVisitor7<VariableElement, Void>
	{
		@Override
		public VariableElement visitVariable(VariableElement variableElement, Void parameter)
		{
			return variableElement;
		}
	}

	/**
	 * <code>TypeVisitor</code> object that provides {@link DeclaredType} instance
	 * when it is accepted by {@link TypeMirror#accept(TypeVisitor, Object)} and
	 * <code>TypeMirror</code> object does not have {@link TypeKind#DECLARED}.
	 */
	private final static DeclaredTypeProviderVisitor
	DECLARED_TYPE_PROVIDER_VISITOR = new DeclaredTypeProviderVisitor();

	private final static ArrayTypeProviderVisitor
	ARRAY_TYPE_PROVIDER_VISITOR = new ArrayTypeProviderVisitor();

	private final static PackageElementProviderVisitor
	PACKAGE_ELEMENT_PROVIDER_VISITOR = new PackageElementProviderVisitor();

	/**
	 * <code>TypeVisitor</code> object that provides {@link DeclaredType} instance
	 * when it is accepted by {@link TypeMirror#accept(TypeVisitor, Object)} and
	 * <code>TypeMirror</code> object does not have {@link TypeKind#DECLARED}.
	 */
	private final static TypeElementProviderVisitor
	TYPE_ELEMENT_PROVIDER_VISITOR = new TypeElementProviderVisitor();

	private final static ExecutableElementProviderVisitor
	EXECUTABLE_ELEMENT_PROVIDER_VISITOR = new ExecutableElementProviderVisitor();

	private final static VariableElementProviderVisitor
	VARIABLE_ELEMENT_PROVIDER_VISITOR = new VariableElementProviderVisitor();

	/**
	 * {@link Comparator} object that compares <code>TypeElement</code> instances
	 * by {@link TypeElement#getQualifiedName()}.
	 */
	public final static Comparator<TypeElement>
	COMPARATOR_TYPE_ELEMENT_BY_QUALIFIED_NAME =
	new Comparator<TypeElement>()
	{
		@Override
		public int compare(TypeElement o1, TypeElement o2)
		{
			return
					o1.getQualifiedName().toString().compareTo(
							o2.getQualifiedName().toString());
		}
	};

	/**
	 * {@link Comparator} object that compares <code>VariableElement</code>
	 * instances by {@link VariableElement#getSimpleName()}.
	 */
	public final static Comparator<VariableElement>
	COMPARATOR_VARIABLE_ELEMENT_BY_SIMPLE_NAME =
	new Comparator<VariableElement>()
	{
		@Override
		public int compare(VariableElement o1, VariableElement o2)
		{
			return
					o1.getSimpleName().toString().compareTo(
							o2.getSimpleName().toString());
		}
	};

	/**
	 * @param typeMirror
	 * @return <code>DeclaredType</code> instance if <code>typeMirror</code>
	 *         object has {@link TypeKind#DECLARED}, <code>null</code> otherwise
	 */
	public static DeclaredType asDeclaredType(TypeMirror typeMirror)
	{
		return typeMirror.accept(DECLARED_TYPE_PROVIDER_VISITOR, null);
	}

	/**
	 * @see #asDeclaredType(TypeMirror)
	 */
	public static DeclaredType asDeclaredType(TypeElement type)
	{
		return asDeclaredType(type.asType());
	}

	public static ArrayType asArrayType(TypeMirror typeMirror)
	{
		return typeMirror.accept(ARRAY_TYPE_PROVIDER_VISITOR, null);
	}

	public static PackageElement asPackageElement(Element element)
	{
		return element.accept(PACKAGE_ELEMENT_PROVIDER_VISITOR, null);
	}

	/**
	 * @param element
	 * @return <code>TypeElement</code> instance if <code>element</code>
	 *         object has {@link ElementKind#CLASS}, <code>null</code> otherwise
	 */
	public static TypeElement asTypeElement(Element element)
	{
		return element.accept(TYPE_ELEMENT_PROVIDER_VISITOR, null);
	}

	/**
	 * @param element
	 * @return <code>ExecutableElement</code> instance if <code>element</code>
	 *         object has {@link ElementKind#METHOD}, <code>null</code> otherwise
	 */
	public static ExecutableElement asExecutableElement(Element element)
	{
		return element.accept(EXECUTABLE_ELEMENT_PROVIDER_VISITOR, null);
	}

	/**
	 * @param element
	 * @return <code>VariableElement</code> instance if <code>element</code>
	 *         object has {@link ElementKind#FIELD}, <code>null</code> otherwise
	 */
	public static VariableElement asVariableElement(Element element)
	{
		return element.accept(VARIABLE_ELEMENT_PROVIDER_VISITOR, null);
	}

	/**
	 * @param typeMirror
	 * @return <code>TypeElement</code> instance if <code>typeMirror</code>
	 *         represents a <code>TypeElement</code>, <code>null</code> otherwise
	 */
	public static TypeElement asTypeElement(TypeMirror typeMirror)
	{
		DeclaredType declaredType = asDeclaredType(typeMirror);

		if (declaredType == null)
		{
			return null;
		}

		return asTypeElement(declaredType.asElement());
	}

	public static List<VariableElement> getFields(TypeElement typeElement)
	{
		return ElementFilter.fieldsIn(typeElement.getEnclosedElements());
	}

	public static List<ExecutableElement> getMethods(TypeElement typeElement)
	{
		return ElementFilter.methodsIn(typeElement.getEnclosedElements());
	}

	/**
	 * @param element
	 * @return type of <code>element</code> as {@link Element} instance, <code>
	 *         null</code> if there is no such instance. The latter is true for
	 *         built-in, non-declared, primitive Java types.
	 */
	public static Element typeOfAsElement(Element element)
	{
		if (element.asType().getKind() == TypeKind.DECLARED)
		{
			return asDeclaredType(element.asType()).asElement();
		}

		return null;
	}

	public static boolean isPackageKind(Element element)
	{
		return element.getKind() == ElementKind.PACKAGE;
	}

	public static boolean isClassKind(Element element)
	{
		return element.getKind() == ElementKind.CLASS;
	}

	public static boolean isInterfaceKind(Element element)
	{
		return element.getKind() == ElementKind.INTERFACE;
	}

	public static boolean isEnumKind(Element element)
	{
		return element.getKind() == ElementKind.ENUM;
	}

	public static boolean isEnumConstantKind(Element element)
	{
		return element.getKind() == ElementKind.ENUM_CONSTANT;
	}

	public static boolean isAnnotationKind(Element element)
	{
		return element.getKind() == ElementKind.ANNOTATION_TYPE;
	}

	public static boolean isMethodKind(Element element)
	{
		return element.getKind() == ElementKind.METHOD;
	}

	public static boolean isFieldKind(Element element)
	{
		return element.getKind() == ElementKind.FIELD;
	}

	public static boolean isAbstract(Element element)
	{
		return element.getModifiers().contains(Modifier.ABSTRACT);
	}

	public static boolean isStatic(Element element)
	{
		return element.getModifiers().contains(Modifier.STATIC);
	}

	public static boolean isFinal(Element element)
	{
		return element.getModifiers().contains(Modifier.FINAL);
	}

	public static boolean isVisibilityPublic(Element element)
	{
		return element.getModifiers().contains(Modifier.PUBLIC);
	}

	public static boolean isVisibilityDefault(Element element)
	{
		Set<Modifier> modifiers = element.getModifiers();

		return
				! (modifiers.contains(Modifier.PUBLIC) ||
						modifiers.contains(Modifier.PROTECTED) ||
						modifiers.contains(Modifier.PRIVATE));
	}

	public static boolean isVisibilityProtected(Element element)
	{
		return element.getModifiers().contains(Modifier.PROTECTED);
	}

	public static boolean isVisibilityPrivate(Element element)
	{
		return element.getModifiers().contains(Modifier.PRIVATE);
	}

	public static boolean isPrimitiveKind(Element element)
	{
		return element.asType().getKind().isPrimitive();
	}

	public static boolean isPrimitiveKind(TypeMirror typeMirror)
	{
		return typeMirror.getKind().isPrimitive();
	}
	
	public static boolean hasTypeParameters(TypeElement type)
	{
		return type.getTypeParameters().size() > 0;
	}

	public static boolean hasTypeParameters(TypeMirror type)
	{
		TypeElement typeElement = asTypeElement(type);

		if (typeElement != null)
		{
			return hasTypeParameters(typeElement);
		}

		return false;
	}

	public static boolean hasTypeArguments(DeclaredType type)
	{
		return type.getTypeArguments().size() > 0;
	}

	public static boolean isDeclaredKind(TypeMirror typeMirror)
	{
		return typeMirror.getKind() == TypeKind.DECLARED;
	}

	/**
	 * @param typeElements
	 * @return those type elements in <code>typeElements</code> that have {@link
	 *         ElementKind#CLASS}
	 */
	public static Set<TypeElement> getClassTypeElements(
			Set<TypeElement> typeElements)
	{
		Set<TypeElement> result = new HashSet<TypeElement>();

		for (TypeElement typeElement : typeElements)
		{
			if (typeElement.getKind().equals(ElementKind.CLASS))
			{
				result.add(typeElement);
			}
		}

		return result;
	}

	/**
	 * @param elements
	 * @return those elements in <code>elements</code> that are <code>
	 *         TypeElements</code>
	 */
	public static Set<TypeElement> getTypeElements(
			Collection<? extends Element> elements)
	{
		Set<TypeElement> result = new HashSet<TypeElement>();

		for (Element element : elements)
		{
			if (element instanceof TypeElement)
			{
				result.add((TypeElement) element);
			}
		}

		return result;
	}

	/**
	 * @param roundEnvironment
	 * @param annotation
	 * @return type elements in <code>roundEnvironment</code> that are annotated
	 *         with annotation and that have {@link ElementKind#CLASS}
	 */
	public static Set<TypeElement> getClassTypeElements(
			RoundEnvironment roundEnvironment,
			Class<? extends Annotation> annotation)
	{
		return
				getClassTypeElements(
						ElementFilter.typesIn(
								roundEnvironment.getElementsAnnotatedWith(annotation)));
	}

	public static String getPackageQualifiedName(Element element)
	{
		return
				getElementUtilities().getPackageOf(element).getQualifiedName().toString();
//		return elementUtilities.getPackageOf(element).getQualifiedName().toString();
	}

	public static PackageElement getPackageOf(Element element)
	{
		return getElementUtilities().getPackageOf(element);
//		return elementUtilities.getPackageOf(element);
	}

	public static String getJavaDocComment(Element element)
	{
//		return elementUtilities.getDocComment(element);
		return getElementUtilities().getDocComment(element);
	}

	public static Element getElement(TypeMirror typeMirror)
	{
//		return typeUtilities.asElement(typeMirror);
		return getTypeUtilities().asElement(typeMirror);
	}

	/**
	 * @param annotationMirror
	 * @param useSimpleName
	 * @return formatted String representation of <code>annotationMirror</code>
	 */
	public static String toString(
			AnnotationMirror annotationMirror, boolean useSimpleName)
	{
		StringBuffer stringBuffer;

		if (useSimpleName)
		{
			stringBuffer =
					new StringBuffer(
							"@" +
									annotationMirror.getAnnotationType().asElement().getSimpleName());
		}
		else
		{
			stringBuffer =
					new StringBuffer(
							"@" +
									annotationMirror.getAnnotationType().asElement().getSimpleName());
		}

		// map with annotation elements as keys and their respective value
		Map<? extends ExecutableElement, ? extends AnnotationValue>
				annotationTypeElementDeclarations2AnnotationValues =
						annotationMirror.getElementValues();

		if (annotationTypeElementDeclarations2AnnotationValues.size() > 0)
		{
			// open new line for key / value pairs in brackets
			stringBuffer.append("\n(\n");

			for (ExecutableElement	executableElement :
				annotationTypeElementDeclarations2AnnotationValues.keySet())
			{
				stringBuffer.append(
						"\t" +
								executableElement.getSimpleName() + " = " +
								annotationTypeElementDeclarations2AnnotationValues.get(
										executableElement) + ",");
			}

			// replace last "," with "\n)"
			stringBuffer.replace(
					stringBuffer.length() - 1, stringBuffer.length(), "\n)");
		}

		return stringBuffer.toString().replace("\\\"", "\"");
	}

	/**
	 * TODO replace with {@link #asDebugString(Element)}?
	 * @param element
	 * @return
	 */
	public static String elementInfo(Element element)
	{
		StringBuffer result =
				new StringBuffer(
						"element.getSimpleName()      : " + element.getSimpleName()       + "\n" +
						"element.getEnclosingElement(): " + element.getEnclosingElement() + "\n" +
						"element.getKind()            : " + element.getKind()             + "\n" +
						"element.asType()             : " + element.asType()              + "\n");

		if (element.asType().getKind() == TypeKind.DECLARED)
		{
			result.append(
					"typeOfAsElement(element)                : " + typeOfAsElement(element)                 + "\n" +
					"typeOfAsElement(element).getSimpleName(): " + typeOfAsElement(element).getSimpleName() + "\n" +
					"type arguments\n");

			DeclaredType declaredType = asDeclaredType(element.asType());

			List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();

			for (TypeMirror typeMirror : typeArguments)
			{
				result.append("\ttypeMirror: " + typeMirror + "\n");
			}
		}

		return result.toString();
	}

	/**
	 * @param typeElement
	 * @param searchedAnnotation
	 * @return declaration of <code>typeElement</code>'s superclasses that
	 *         are annotated with <code>searchedAnnotation</code>
	 */
	public static TypeElement[] getSuperClassesAnnotatedWith(
			TypeElement typeElement, Class<?> searchedAnnotation)
	{
		List<TypeElement> result = new ArrayList<TypeElement>();

		TypeMirror superClassType = typeElement.getSuperclass();

		// the following condition covers super class == java.lang.Object
		if ((superClassType == null) || (superClassType.getKind() == TypeKind.NONE))
		{
			return result.toArray(new TypeElement[0]);
		}

		TypeElement superTypeElement = asTypeElement(superClassType);

		//		if (superTypeElement.getQualifiedName().equals(Object.class.getName()))
		//		{
		//			return result.toArray(new TypeElement[0]);
		//		}

		// check the annotations of superClassDeclaration
		List<? extends AnnotationMirror> existingAnnotations =
				superTypeElement.getAnnotationMirrors();

		for (AnnotationMirror annotationMirror : existingAnnotations)
		{
			if (annotationMirror.getAnnotationType()
					.toString()
					.equals(searchedAnnotation.getName()))
			{
				// found annotation, add to result
				result.add(superTypeElement);
			}
		}

		// recursive call with same annotation and superClassDeclaration
		result.addAll(
				Arrays.asList(
						getSuperClassesAnnotatedWith(
								superTypeElement, searchedAnnotation)));

		return result.toArray(new TypeElement[0]);
	}

	public static TypeMirror getTypeMirror(String qualifiedTypeName)
	{
		qualifiedTypeName = normalizeBinaryQualifiedTypeName(qualifiedTypeName);

		TypeElement typeElement =
				getElementUtilities().getTypeElement(qualifiedTypeName);

		if (typeElement == null)
		{
			return null;
		}

		return typeElement.asType();
	}
	
	public static TypeMirror getTypeMirror(Class<?> clazz)
	{
		return getTypeMirror(clazz.getCanonicalName());
	}

	public static boolean isSubtype(
			TypeMirror supposedSubtype, TypeMirror supposedSupertype)
	{
		return getTypeUtilities().isSubtype(supposedSubtype, supposedSupertype);
	}

	public static boolean isSubtype(
			TypeElement supposedSubtype, TypeElement supposedSupertype)
	{
		return isSubtype(supposedSubtype.asType(), supposedSupertype.asType());
	}

	public static boolean isAssignableType(
			TypeMirror rightHandType, TypeMirror leftHandType)
	{
		return getTypeUtilities().isAssignable(rightHandType, leftHandType);
	}

	public static boolean isSupertype(
			TypeElement supposedSupertype, TypeElement supposedSubtype)
	{
		return isSupertype(supposedSupertype.asType(), supposedSubtype.asType());
	}

	public static boolean isSupertype(
			TypeMirror supposedSupertype, TypeMirror supposedSubtype)
	{
		if (isSupertypeDirect(supposedSupertype, supposedSubtype))
		{
			return true;
		}

		// direct super types did not contain supposed super type
		// call this method recursively for supposed super type and each direct
		// super type until one of these calls returns true
		for (TypeMirror directSuperType :
		     getTypeUtilities().directSupertypes(supposedSubtype))
		{
			if (isSupertype(supposedSupertype, directSuperType))
			{
				return true;
			}
		}

		return false;
	}

	public static boolean isSupertypeDirect(
			TypeElement supposedSupertype, TypeElement supposedSubtype)
	{
		return
				isSupertypeDirect(supposedSupertype.asType(), supposedSubtype.asType());
	}

	public static boolean isSupertypeDirect(
			TypeMirror supposedSupertype, TypeMirror supposedSubtype)
	{
		for (TypeMirror directSuperType :
		     getTypeUtilities().directSupertypes(supposedSubtype))
		{
			// TODO normally the following should work perfectly but tests showed that it is
			//      not possible to find out that Collection is a super type of Set this way
			//			if (typeUtilities.isSameType(supposedSupertype, directSuperTypeMirror))
			if (supposedSupertype.toString().equals(directSuperType.toString()))
			{
				return true;
			}
		}

		return false;
	}

	public static boolean isSameType(
			TypeMirror typeMirror1, TypeMirror typeMirror2)
	{
		// TODO normally the following should work perfectly but tests showed that
		//      it is not possible to find out that Collection is a super type of
		//      Set this way
		// return typeUtilities.isSameType(typeMirror1, typeMirror2)
		return typeMirror1.toString().equals(typeMirror2.toString());
	}

	public static TypeMirror getOuterType(TypeMirror typeMirror)
	{
		TypeMirror result = typeMirror;

		DeclaredType declaredType = ProcessingUtilities.asDeclaredType(typeMirror);

		if (declaredType != null)
		{
			String qualifiedNameType = declaredType.toString();

			int index = qualifiedNameType.indexOf('<');

			if (index > -1)
			{
				String qualifiedNameOuterType = qualifiedNameType.substring(0, index);
				result = getTypeMirror(qualifiedNameOuterType);
			}
		}

		return result;
	}

	/**
	 * @param element
	 * @return type mirror of <code>element</code>'s single type argument, <code>
	 *         null</code> if there is not exactly one type argument
	 */
	public static TypeMirror getTypeMirrorOfTypeArgument(Element element)
	{
		DeclaredType declaredType = asDeclaredType(element.asType());

		List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();

		if (typeArguments.size() != 1)
		{
			return null;
		}

		return typeArguments.iterator().next();
	}

	public static TypeElement getEnclosingTypeElement(TypeElement typeElement)
	{
		return asTypeElement(typeElement.getEnclosingElement());
	}

	/**
	 * Looks for type element with qualified name equal to <code>
	 * enclosedElementQualifiedName</code> among elements enclosed by and
	 * including <code>enclosingElement</code>. Does not recurse into sub
	 * packages.
	 *
	 * @param enclosingElement
	 * @param enclosedElementQualifiedName
	 * @return see above
	 */
	public static TypeElement findInEnclosedTypeElement(
			Element enclosingElement, String enclosedElementQualifiedName)
	{
		if (enclosingElement instanceof TypeElement)
		{
			TypeElement candidate = (TypeElement) enclosingElement;

			if (checkTypeElementForQualifiedName(
					candidate, enclosedElementQualifiedName))
			{
				return candidate;
			}

			for (Element element : candidate.getEnclosedElements())
			{
				// recurse into contained elements
				TypeElement containedCandidate =
						findInEnclosedTypeElement(element, enclosedElementQualifiedName);

				if (containedCandidate != null)
				{
					return containedCandidate;
				}
			}
		}

		if (enclosingElement instanceof PackageElement)
		{
			return findInEnclosedElement((PackageElement) enclosingElement, enclosedElementQualifiedName);
		}

		return null;
	}

	/**
	 * Looks for type element with qualified name equal to <code>
	 * enclosedElementQualifiedName</code> among elements enclosed by <code>
	 * enclosingElement</code>. Does not recurse into sub packages.
	 *
	 * @param enclosingElement
	 * @param enclosedElementQualifiedName
	 * @return see above
	 */
	public static TypeElement findInEnclosedElement(
			PackageElement enclosingElement, String enclosedElementQualifiedName)
	{
		for (Element element : enclosingElement.getEnclosedElements())
		{
			if (element instanceof TypeElement)
			{
				TypeElement candidate = (TypeElement) element;

				if (checkTypeElementForQualifiedName(
						candidate, enclosedElementQualifiedName))
				{
					return candidate;
				}
			}
		}

		return null;
	}

	public static boolean checkTypeElementForQualifiedName(
			TypeElement typeElement, String qualifiedName)
	{
		return
				typeElement.getQualifiedName().toString().equals(
						normalizeBinaryQualifiedTypeName(qualifiedName));
	}

	public static TypeElement getTypeElement(Class<?> clazz)
  {
	  return getElementUtilities().getTypeElement(clazz.getCanonicalName());
  }

	public static WildcardType getWildcardType(
	    TypeMirror extendsBound, TypeMirror superBound)
	{
		return getTypeUtilities().getWildcardType(extendsBound, superBound);
	}

	public static DeclaredType getDeclaredType(
      TypeElement typeElement, TypeMirror[] types)
  {
	  return getTypeUtilities().getDeclaredType(typeElement, types);
  }

//	public static TypeElement getTypeElementCollection()
//	{
//		return asTypeElement(getProcessingUtilities().declaredTypeCollection);
//	}
//
//	public static TypeElement getTypeElementMap()
//	{
//		return asTypeElement(getProcessingUtilities().declaredTypeMap);
//	}
//
//	public static TypeElement getTypeElementVoid()
//	{
//		return asTypeElement(getProcessingUtilities().declaredTypeVoid);
//	}

	public static boolean isA(
			TypeMirror supposedSubTypeMirror, TypeElement baseTypeElement)
	{
		return isA(asTypeElement(supposedSubTypeMirror), baseTypeElement);
	}

	public static boolean isA(
			TypeElement supposedSubTypeElement, TypeElement baseTypeElement)
	{
		if (supposedSubTypeElement == null)
		{
			throw new IllegalArgumentException("supposed sub type element must not be null");
		}
		if (baseTypeElement == null)
		{
			throw new IllegalArgumentException("base type element must not be null");
		}

		int genericsCountSupposedSubTypeElement = supposedSubTypeElement.getTypeParameters().size();

		// fill the right number of types with wildcard type null
		TypeMirror[] types = new TypeMirror[genericsCountSupposedSubTypeElement];

		for (int i = 0; i < genericsCountSupposedSubTypeElement; i++)
		{
			types[i] = getWildcardTypeNull();
		}

		// Locate the correct DeclaredType to match with the type
		DeclaredType declaredTypeSupposedSubType =
				getDeclaredType(supposedSubTypeElement, types);

		int genericsCountBaseTypeElement = baseTypeElement.getTypeParameters().size();

		// fill the right number of types with wild card null
		types = new TypeMirror[genericsCountBaseTypeElement];

		for (int i = 0; i < genericsCountBaseTypeElement; i++)
		{
			types[i] = getWildcardTypeNull();
		}

		// Locate the correct DeclaredType to match with the type
		DeclaredType declaredTypeBaseType = getDeclaredType(baseTypeElement, types);

		// Is the given type able to be assigned as the typeElement?
		return
				isAssignableType(declaredTypeSupposedSubType, declaredTypeBaseType);
	}

	public static boolean isA(
			TypeElement supposedSubTypeMirror, TypeMirror baseTypeMirror)
	{
		return isA(supposedSubTypeMirror, asTypeElement(baseTypeMirror));
	}

	public static boolean isA(
			TypeMirror supposedSubTypeMirror, TypeMirror baseTypeMirror)
	{
		return isA(supposedSubTypeMirror, asTypeElement(baseTypeMirror));
	}

	public static Filer getFiler()
	{
		return getProcessingUtilities().processingEnvironment.getFiler();
	}
	
	public static boolean isProcessingOver()
	{
		return getProcessingUtilities().roundEnvironment.processingOver();
	}

	public static Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> annotationClass)
	{
		return getProcessingUtilities().roundEnvironment.getElementsAnnotatedWith(annotationClass);
	}

	public static Set<? extends TypeElement> getSupportedAnnotations()
	{
		return getProcessingUtilities().supportedAnnotations;
	}

	public static ProcessingEnvironment getProcessingEnvironment()
	{
		return getProcessingUtilities().processingEnvironment;
	}

	public static RoundEnvironment getRoundEnvironment()
	{
		return getProcessingUtilities().roundEnvironment;
	}

	public static Set<? extends Element> getRootElements()
	{
		return getProcessingUtilities().roundEnvironment.getRootElements();
	}

	/**
	 * @param annotation
	 * @return enclosing type elements for elements annotated with <code>
	 *         annotation</code>
	 */
	public static Set<TypeElement> getEnclosingTypeElementsForElementsAnnotatedWith(
			Class<? extends Annotation> annotation)
	{
		Set<TypeElement> result = new HashSet<TypeElement>();

		for (Element element :
		     getProcessingUtilities().roundEnvironment.getElementsAnnotatedWith(annotation))
		{
			TypeElement enclosingTypeElement =
					ProcessingUtilities.asTypeElement(element.getEnclosingElement());

			// enclosing element might not be a type element, if so
			// enclosingTypeElement is null and must not be added to result
			if (enclosingTypeElement != null)
			{
				result.add(enclosingTypeElement);
			}
		}

		return result;
	}

//	/**
//	 * @param packages
//	 * @param types
//	 * @return Those type elements from round environment that are
//	 *         <ul>
//	 *           <li>from one of the packages in <code>packages</code> or</li>
//	 *           <li>correspond to one of the types in <code>types</code>.</li>
//	 *         </ul>
//	 *         In other words: Those type elements that correspond to all the
//	 *         types in all the <code>packages</code> plus the type elements that
//	 *         correspond to <code>types</code>.
//	 */
//	public static Set<TypeElement> getTypeElements(
//			Set<Package> packages, Set<? extends Class<?>> types)
//  {
//		Set<TypeElement> result = new HashSet<>();
//		
//		Set<TypeElement> allAnnotatedTypeElements =
//				getTypeElements(
//		    		 getProcessingUtilities().roundEnvironment
//		                                 .getElementsAnnotatedWith(
//		                            		     MarkerAnnotationType.class));
//
//		Set<String> packageNames = asPackageNameSet(packages);
//
//		for (TypeElement annotatedTypeElement : allAnnotatedTypeElements)
//		{
//			PackageElement packageOfAnnotatedElement =
//					getPackageOf(annotatedTypeElement);
//
//			Set<TypeElement> typesInPackageOfAnnotatedElement =
//					getTypeElements(packageOfAnnotatedElement.getEnclosedElements());
//
//			if (packageNames.contains(
//			    packageOfAnnotatedElement.getQualifiedName().toString()))
//			{
//				// the package of the annotated element belongs to the packages in the
//				// parameters, all types in the package have to be added to result
//				result.addAll(typesInPackageOfAnnotatedElement);
//			}
//			else
//			{
//				// the package of the annotated element does not belong to the packages
//				// in the parameters, so the types in the parameter have to be searched
//				// in the types of the package
//				// search will include enclosed type elements
//				result.addAll(
//						findTypesInPackageTypes(
//								types,
//								withEnclosedTypeElements(typesInPackageOfAnnotatedElement)));
//			}
//		}
//		
//		return result;
//  }
	
	public static Set<TypeElement> withEnclosedTypeElements(
			Set<TypeElement> typeElements)
	{
		Set<TypeElement> result = new HashSet<>();
		
		for (TypeElement typeElement : typeElements)
		{
			// add type element
			result.add(typeElement);
			// add enclosed type elements recursively
			result.addAll(
					withEnclosedTypeElements(
							getTypeElements(typeElement.getEnclosedElements())));
		}

		return result;
	}
	
	public static Set<TypeElement> getSubtypes(
			TypeElement superType, Set<TypeElement> typeElements)
	{
		Set<TypeElement> result = new HashSet<>();
		
		for (TypeElement typeElement : typeElements)
    {
	    if (isSubtype(typeElement, superType))
	    {
	    	result.add(typeElement);
	    }
    }
		
		return result;
	}

	public static String normalizeBinaryClassName(Class<?> clazz)
	{
		return normalizeBinaryQualifiedTypeName(clazz.getName());
	}

	public static String normalizeBinaryQualifiedTypeName(String name)
	{
		return name.replaceAll("\\$", "\\.");
	}

	public static Set<Element> asElementSet(Set<TypeElement> typeElements)
	{
		Set<Element> result = new HashSet<>();
		result.addAll(typeElements);
		return result;
	}

	public static Set<Element> asElementSet(List<Element> elements)
	{
		Set<Element> result = new HashSet<>();
		result.addAll(elements);
		return result;
	}

	/**
	 * TODO replace with {@link #elementInfo(Element)}?
	 * @param element
	 * @return
	 */
	public static String asDebugString(Element element)
	{
		VariableElement variableElement = asVariableElement(element);
		
		if (variableElement != null)
		{
			Element enclosingElement = element.getEnclosingElement();
			
			TypeElement enclosingTypeElement = asTypeElement(enclosingElement);
		
			return
					enclosingTypeElement.getQualifiedName().toString() + "." +
					element.getSimpleName();
		}
		
		ExecutableElement executableElement = asExecutableElement(element);
		
		if (executableElement != null)
		{
			Element enclosingElement = element.getEnclosingElement();

			TypeElement enclosingTypeElement = asTypeElement(enclosingElement);

			return
					enclosingTypeElement.getQualifiedName().toString() + "." +
					executableElement.getSimpleName() +
					"(" + parametersAsDebugString(executableElement.getParameters()) + ")";
		}

		TypeElement typeElement = asTypeElement(element);
		
		if (typeElement != null)
		{
			return typeElement.getQualifiedName().toString();
		}

		PackageElement packageElement = asPackageElement(element);
			
		if (packageElement != null)
		{
			return packageElement.getQualifiedName().toString();
		}
		
		return
				ProcessingUtilities.class.getName() +
				".asDebugString(Element): unexpected element type " +
						element.getKind();
	}

	private static String parametersAsDebugString(
	    List<? extends VariableElement> parameters)
	{
		List<String> parametersAsStrings = new ArrayList<>();
		
		for (VariableElement parameter : parameters)
    {
	    parametersAsStrings.add(
	    		parameter.asType().toString() + " " + parameter.getSimpleName());
    }
		
		return StringUtils.join(parametersAsStrings, ", ");
	}

	private static ProcessingUtilities getProcessingUtilities()
	{
		if (processingUtilities == null)
		{
			throw new IllegalStateException(
					ProcessingUtilities.class.getName() + " are not initialised, access " +
					"is possible from within running " + Processor.class.getName() +
					" instances only");
		}
		
		return processingUtilities;
	}

	private static Elements getElementUtilities()
	{
		return getProcessingUtilities().elementUtilities;
	}

	private static Types getTypeUtilities()
	{
		return getProcessingUtilities().typeUtilities;
	}

	private static WildcardType getWildcardTypeNull()
	{
		ProcessingUtilities processingUtilities = getProcessingUtilities();

		if (processingUtilities.wildcardTypeNull == null)
		{
			processingUtilities.wildcardTypeNull = getWildcardType(null, null);
		}
		
		return processingUtilities.wildcardTypeNull;
	}
}